package search.service.search;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Component;
import search.config.AnalyzerNames;
import common.config.IndexTypeNames;
import search.config.SearchParameters;
import common.dao.elastic.ElasticSearchDao;
import common.dao.gremlin.TitanGraphDao;
import common.helper.json.JsonFormatter;
import common.helper.serialization.FormatOfDate;
import search.model.query.ComplexParameter;
import search.service.analysis.AnalysisService;
import search.service.analysis.EncodeDecode;
import search.service.analysis.QueryCombinator;
import search.service.analysis.QueryExpansion;

import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Created by julianzliu on 2016/7/3.
 */
@Component
public class ComplexSearchService_V2 {
    final Logger logger = LoggerFactory.getLogger(ComplexSearchService_V2.class);

    //private final ObjectMapper mapper;
    private final ElasticSearchDao elasticsearchDao;
    private final TitanGraphDao titanGraphDao;
    /*
    * common config
     */
    String must_analyzer = AnalyzerNames.ik_max_word;
    String base_analyzer = AnalyzerNames.ik_smart;
    String expa_analyzer = AnalyzerNames.ik_smart;
    String base_minimum = SearchParameters.MinimumMatch.default_minimum;  //-1
    String expa_minimum = SearchParameters.MinimumMatch.exp_minimum;  //100%
    String sub_base_minimum = SearchParameters.MinimumMatch.ori_minimum; //"-1";
    String sub_expa_minimum = SearchParameters.MinimumMatch.exp_minimum; //"100%";
    String sub_base_analyzer = AnalyzerNames.ik_smart;
    String sub_expa_analyzer = AnalyzerNames.ik_smart;
    float whole_boost = SearchParameters.Boosting.whole;
    float parts_boost = SearchParameters.Boosting.parts;
    float subquery_boost = SearchParameters.Boosting.subquery;

    @Inject
    public ComplexSearchService_V2(
            ElasticSearchDao elasticsearchDao
            //,TitanGraphDao titanGraphDao

    ) {
        this.elasticsearchDao = elasticsearchDao;
        //this.titanGraphDao = titanGraphDao;
        /*
        Titan Need To Be Replace By OrientDB
         */
        this.titanGraphDao = null;
    }


    private BoolQueryBuilder singleQueryBulder(String text, String filed,
                                              String base_minimum, String expo_minimum,String base_analyzer, String expo_analyzer){
        BoolQueryBuilder whole = QueryCombinator.combineBaseAndExpansionForMatchQuery(
                this.elasticsearchDao,
                text,filed,base_minimum,expo_minimum, base_analyzer,expo_analyzer
        );
        return whole;
    }
    private BoolQueryBuilder partsQueryBulder(String text, String[] fields,
                                              String base_minimum, String expo_minimum, String base_analyzer, String expo_analyzer){
        BoolQueryBuilder parts = QueryCombinator.combineBaseAndExpansionForMultiMatchQuery(
                this.elasticsearchDao,
                text, fields, base_minimum,expo_minimum, base_analyzer,expo_analyzer
        );
        return parts;
    }

    /*
    针对在index时，通过 copy_to 创建的字段的 查询，
    不使用查询扩展
    single generated field query,
    without query expansion
     */
    private void setWholeQuery(BoolQueryBuilder query, String text, String field) {
        BoolQueryBuilder whole = QueryExpansion.queryWithMatchQuery(text,field,sub_base_minimum,sub_base_analyzer);
        query.should(whole.boost(whole_boost));
    }

    /*
    针对一个查询字段涉及多个原始索引字段的查询，
    使用查询扩展
    multi field query,
    with multiple expansion
     */
    private void setPartsQuery(BoolQueryBuilder query, String text, String[] fields){
        BoolQueryBuilder parts = partsQueryBulder(
                text, fields,
                sub_base_minimum,sub_expa_minimum,sub_base_analyzer,sub_expa_analyzer
        );
        query.should(parts.boost(parts_boost));
    }

    /*
    针对一个查询字段对应一个原始索引字段的查询，
    使用查询扩展
    single field query
    with multiple expansion
     */
    private void setSingleQuery(BoolQueryBuilder query, String text, String field){
        BoolQueryBuilder whole = singleQueryBulder(
                text, field,
                sub_base_minimum, sub_expa_minimum, sub_base_analyzer, sub_expa_analyzer
        );
        query.should(whole.boost(parts_boost));
    }


    private void setWholeAndPartsQuery(BoolQueryBuilder query, String text, String whole_field, String[] parts_fields){
        setWholeQuery(query,text,whole_field);
        setPartsQuery(query,text,parts_fields);
    }




    //*******************************************************************************




    /**
     *
     * @param param
     * @return
     */
    public String complexSearch(ComplexParameter param) {
        logger.debug("complexSearch , param = \n {} ", param.toString());

        /**************************************************/
        BoolQueryBuilder query = QueryBuilders.boolQuery();

        /*
        最外层query受内层sub_query设置的影响，
        minimum在不明确设置的情况下是 100%,
        需要明确设置为 0，
        只需匹配must子查询即可
         */
        // no need for should_sub_match
        query.minimumShouldMatch("0");

        /***********************************
        * <minimum>
         */
        if(param.getMinimum()!=null && !param.getMinimum().isEmpty()) {
            base_minimum = param.getMinimum();
            expa_minimum = param.getMinimum();
        }


        /***********************************
        * <levels>
         */
        List<String> ori_levels = param.getLevels();
        Boolean isProgram = false , isSequence = false, isScene = false, isShot = false;
        List<String> levels = new ArrayList<String>();
        logger.debug("param.levels.size = {} \n" , param.getLevels().size());
        if(ori_levels.size() == 0){
            isProgram = isSequence = isScene = isShot = true;
            levels = IndexTypeNames.MediaDB.getTableTypeList();
        }
        else if(ori_levels.contains( IndexTypeNames.MediaDB.ProgramSetName )) {
            isProgram = true;
            levels.add(IndexTypeNames.MediaDB.ProgramSetName);
        }
        else if(ori_levels.contains( IndexTypeNames.MediaDB.SequenceSetName )){
            isSequence = true;
            levels.add(IndexTypeNames.MediaDB.SequenceSetName);
        }
        else if(ori_levels.contains( IndexTypeNames.MediaDB.SceneSetName )){
            isScene = true;
            levels.add(IndexTypeNames.MediaDB.SceneSetName);
        }
        else if(ori_levels.contains( IndexTypeNames.MediaDB.ShotSetName )){
            isShot = true;
            levels.add(IndexTypeNames.MediaDB.ShotSetName);
        }


        /************************************
        * <fulltext>
         * 对应搜索框字段
         */
        String fulltext = param.getFulltext();
        if(fulltext != null && !fulltext.equals("") ){
            logger.info("[fulltext] fulltext = {}", fulltext);
            String q = fulltext;
            BoolQueryBuilder multi = QueryBuilders.boolQuery();
            BoolQueryBuilder must_query = QueryBuilders.boolQuery();
            BoolQueryBuilder should_query = QueryBuilders.boolQuery();
            String field = "_all";
            List<String> tokenList = AnalysisService.getTokenTermList(this.elasticsearchDao, q, must_analyzer);
            int tokenLength = tokenList.size();
            /*
            0) ik_max_word
            base_query
            a. minimum 默认是 -1
            b. ik_smart分词结果 不一定是 ik_max_word的子集
            */
            BoolQueryBuilder base_query = QueryExpansion.queryWithMatchQuery(q, field, base_minimum, must_analyzer);
            if(tokenLength <= 1)
                must_query.should(
                        base_query.boost(SearchParameters.Boosting.original_base_query)
                );
            else
                must_query.must(
                        base_query.boost(SearchParameters.Boosting.original_base_query)
                );

            /*
            2) ik_smart_syno
             */
            BoolQueryBuilder syno_query = QueryExpansion.queryExpansionWithSynoWithMatchQuery(
                    this.elasticsearchDao, q, field,
                    expa_minimum, expa_analyzer);
            must_query.must(
                    syno_query.boost(SearchParameters.Boosting.original_syno_query)
            );

            /*
            3) using analyzers to expansion query...
               with semantic dict...
             */
            BoolQueryBuilder expansion_query = QueryBuilders.boolQuery();
            try{
                expansion_query = QueryExpansion.queryExpansionWithDictForMatchQuery(
                        this.elasticsearchDao,
                        q,field,expa_minimum,expa_analyzer);
            }catch (Exception e){
                e.printStackTrace();
            }
            should_query.should(
                    expansion_query.boost(SearchParameters.Boosting.expasion_query)
            );

            // 4) field_query
            BoolQueryBuilder fields_query = QueryExpansion.queryWithFieldsMatchQuery(q,base_analyzer);
            should_query.should(
                    fields_query
            );


            // merge must_query and should_query
            multi.must( must_query );
            multi.should( should_query );

            /*
            should to final query ...
             */
            query.must(
                    multi.boost(SearchParameters.Boosting.fulltext)
            );
            logger.info("[fulltext] success! ----------->>>>>>>>>>>");
        }
        else{
            MatchAllQueryBuilder base = QueryBuilders.matchAllQuery();
            query.should(base);
        }



        /**********************************
         * <all_field>
         * 所有字段的合并，从全局上提供信号
         */
        String all_field = param.getAllField();
        if(all_field!=null && !all_field.isEmpty()){
            logger.info("[all_field]");
            BoolQueryBuilder subquery = QueryCombinator.combineBaseAndExpansionForMatchQuery(
                    this.elasticsearchDao,
                    all_field,"_all",base_minimum,expa_minimum, base_analyzer, expa_analyzer
            );
            query.should(
                    subquery.boost(SearchParameters.Boosting.allfield)
            );
        }


        /*
        *
        * !!!
        * <knowledge>
        *     query expansion using knowledge graph,
        *     based on all_field
        *
         */
        boolean GRAPH_EXPAN = true;
        if(GRAPH_EXPAN && all_field != null && !all_field.equals("")){
            BoolQueryBuilder subquery = QueryBuilders.boolQuery();
            boolean success = false;
            try{
                logger.info("[Graph] ERROR FOUND HERE FINALLY!");
                subquery = QueryExpansion.queryExpansionWithGraph(this.elasticsearchDao, this.titanGraphDao
                        , all_field, "_all", expa_minimum, AnalyzerNames.ik_smart);
                success = true;
            }catch (Exception e){
                //e.printStackTrace();
                logger.error("[complexSearch] Expansion Using Knowledge Graph Failed! Ignore the Expansion!");
                logger.error( e.toString() );
            }
            if(success)
                query.should( subquery );
        }


        /*
        * <pinyin>
         */
        String pinyin = param.getPinyin();
        if(pinyin!=null && !pinyin.isEmpty()){
            MatchQueryBuilder subquery = QueryBuilders.matchQuery(
                    "Indexdata.AllField.pinyin",
                    param.getPinyin()
            );
            subquery
                    .analyzer("ik_smart_pinyin")
                    .minimumShouldMatch("100%");
            query.must(
                    subquery.boost(SearchParameters.Boosting.pinyin)
            );
        }



        /*
        * <source>
         */
        String source = param.getSource();
        if(source!=null && !source.isEmpty()){
            BoolQueryBuilder subquery = QueryBuilders.boolQuery();
            int counter = 0;
            /*
            common : Metadata.VendorName
             */
            // TODO: add support for VendorName
            MatchQueryBuilder vendor = QueryBuilders.matchQuery(
                    "Metadata.VendorName",
                    source
            );
            subquery.should( vendor );
            counter += 1;
            if(isProgram){
                setWholeQuery(subquery,source,"Metadata.Program.SearchSource");
                String[] fields = {
                        "Metadata.Program.Copyright.NameofCopyrightOwner"
                };
                setPartsQuery(subquery,source,fields);
                counter++;
            }
            if(isSequence){
                setWholeQuery(subquery,source,"Metadata.Sequence.SearchSource");
                String[] fields = {
                        "Metadata.Sequence.Copyright.NameofCopyrightOwner"
                };
                setPartsQuery(subquery,source,fields);
                counter++;
            }
            if(counter > 0)
                query.must(subquery.boost(SearchParameters.Boosting.source));
        }


        /*
        * <meidiaform>
        *     类型
         */
        String meidiaform = param.getMediaform();
        if(meidiaform!=null && !meidiaform.isEmpty()){
            BoolQueryBuilder subquery = QueryBuilders.boolQuery();
            int counter = 0;
            setSingleQuery(subquery, meidiaform, "Indexdata.ResourceType");
            counter++;
            if(isProgram){
                setWholeQuery(subquery,meidiaform,"Metadata.Program.SearchType");
                String[] fields = {
                        "Metadata.Program.Type.ProgramType",
                        "Metadata.Program.Type.ProgramForm",
                        "Metadata.Program.Description.VersionDescription"
                };
                setPartsQuery(subquery,meidiaform,fields);
                counter++;
            }
            if(isSequence){
                setSingleQuery(subquery,meidiaform,"Metadata.Sequence.Type.ProgramForm");
                counter++;
            }
            if(counter > 0)
                query.must(subquery.boost(subquery_boost));
        }


        /*
        * <subject>
         */
        String subject = param.getSubject();
        if(subject!=null && !subject.isEmpty()){
            String text = subject;
            int counter = 0;
            BoolQueryBuilder subquery = QueryBuilders.boolQuery();
            if(isProgram){
                setWholeQuery(subquery,text,"Metadata.Program.SearchSubject");
                String[] fields = {
                        "Metadata.Program.Subject.SearchClass^3.0f",
                        "Metadata.Program.Subject.Keyword^2.0f",
                        "Metadata.Program.Subject.SubjectTerm^2.0f"
                };
                setPartsQuery(subquery,text,fields);
                counter++;
            }
            if(isSequence){
                setWholeQuery(subquery,text,"Metadata.Sequence.SearchSubject");
                String[] fields = {
                        "Metadata.Sequence.Subject.SearchClasst3.0f",
                        "Metadata.Sequence.Subject.SubjectTerm^2.0f",
                        "Metadata.Sequence.Subject.Keyword^2.0f"
                };
                setPartsQuery(subquery,text,fields);
                counter++;
            }
            if(isScene){
                setWholeQuery(subquery, text, "Metadata.Scene.SearchSubject");
                String[] fields = {
                        "Metadata.Scene.Subject.SubjectTerm",
                        "Metadata.Scene.Subject.Keyword"
                };
                setPartsQuery(subquery, text, fields);
                counter++;
            }
            if(isShot){
                setWholeQuery(subquery,text,"Metadata.Shot.SearchSubject");
                String[] fields = {
                        "Metadata.Shot.Subject.SubjectTerm",
                        "Metadata.Shot.Subject.Keyword"
                };
                setPartsQuery(subquery, text, fields);
                counter++;
            }
            if(counter > 0)
                query.must(subquery.boost(SearchParameters.Boosting.subject));
        }

        /*
        * <region>
         */
        String region = param.getRegion();
        if(region!=null && !region.isEmpty()){
            String text = region;
            Integer counter = 0;
            BoolQueryBuilder subquery = QueryBuilders.boolQuery();
            if(isProgram){
                String[] fields = {
                        "Metadata.Program.Publisher.SearchDescriptionofPublisher",  //1.6.1
                        "Metadata.Program.Publisher.SearchDescriptionofProducer",   //1.6.2
                        "Metadata.Program.SearchCoverage"
                };
                setPartsQuery(subquery, text, fields);
                counter++;
            }
            if(isSequence){
                String[] fields = {
                        "Metadata.Sequence.Publisher.DescriptionofPublisher",  //2.6.1
                        "Metadata.Sequence.Publisher.DescriptionofProducer",   //2.6.2
                        "Metadata.Sequence.SearchCoverage"  //2.12
                };
                setPartsQuery(subquery, text, fields);
                counter++;
            }
            if(isShot){
                setSingleQuery(subquery,text, "Metadata.Shot.Description.ShootingPlace" );
                counter++;
            }
            if(counter>0)
                query.must(subquery.boost(subquery_boost));
        }

        /*
        * <mediasize>
         */
        String mediasize = param.getMediasize();
        if(mediasize!=null && !mediasize.isEmpty()){
            String text = mediasize;
            Integer counter = 0;
            BoolQueryBuilder subquery = QueryBuilders.boolQuery();
            if(isProgram){
                setWholeQuery(subquery,text,"Metadata.Program.SearchFormat");
                String[] fields = {
                        "Metadata.Program.Format.AudioQuality",  //1.11.6
                        "Metadata.Program.Format.VideoQuality"
                };
                setPartsQuery(subquery,text,fields);
                counter++;
            }
            if(counter > 0)
                query.must(subquery.boost(subquery_boost));
        }

        /*
        * <format>
        *     格式
         */
        String format = param.getFormat();
        if(format!=null && !format.isEmpty()){
            String text = format;
            Integer counter = 0;
            BoolQueryBuilder subquery = QueryBuilders.boolQuery();
            setSingleQuery(subquery, text, "Indexdata.VideoFormat");
            counter += 1;
            if(isProgram){
                // 无扩展
                setWholeQuery(subquery, text , "Metadata.Program.SearchFormat");
                setSingleQuery(subquery, text, "Metadata.Program.Format.FileFormat");
                counter++;
            }
            // using VideoPath to get format...
            counter++;
            if(counter > 0)
                query.must(subquery.boost(subquery_boost));
        }

        /*
        * <copyright>
         */
        String copyright = param.getCopyright();
        if(copyright!=null && !copyright.isEmpty()){
            String text = copyright;
            Integer counter = 0;
            BoolQueryBuilder subquery = QueryBuilders.boolQuery();
            setSingleQuery(subquery, text, "Indexdata.Copyright");
            counter++;
            if(isProgram){
                setSingleQuery(subquery,text,"Metadata.Program.SearchCopyright");
                counter++;
            }
            if(isSequence){
                setSingleQuery(subquery,text,"Metadata.Sequence.SearchCopyright");
                counter++;
            }
            if(counter>0)
                query.must(subquery.boost(subquery_boost));
        }



        //****************************************
        AndQueryBuilder filter = QueryBuilders.andQuery();

        /*
        * <shotdate>
         */
        Date shotdatefrom = param.getShotdatefrom();
        Date shotdateto = param.getShotdateto();
        if(shotdatefrom!=null || shotdateto!=null){
            OrQueryBuilder subfilter = QueryBuilders.orQuery();
            if(isProgram){
                RangeQueryBuilder base = QueryBuilders.rangeQuery("Metadata.Program.Date.ProducedDate");
                if(param.getShotdatefrom() != null)
                    base.gte(
                            FormatOfDate.formatWithTime.format(param.getShotdatefrom())
                    );
                if(param.getShotdateto() != null)
                    base.lte(
                            FormatOfDate.formatWithTime.format(param.getShotdateto())
                    );
                subfilter.add(base);
            }
            if(isShot){
                RangeQueryBuilder base = QueryBuilders.rangeQuery("Metadata.Shot.Date.ShootingDate");
                if(param.getShotdatefrom() != null)
                    base.gte(
                            FormatOfDate.formatWithTime.format(param.getShotdatefrom())
                    );
                if(param.getShotdateto() != null)
                    base.lte(
                            FormatOfDate.formatWithTime.format(param.getShotdateto())
                    );
                subfilter.add(base);
            }
            filter.add(subfilter);
        }

        /*
        * <mediatype>
         */
        String mediatype = param.getMediatype();
        if(mediatype!=null && !mediatype.isEmpty()){
            mediatype = SearchParameters.MediaTypes.getMeidiaType(mediatype);
        }
        else{
            mediatype = SearchParameters.MediaTypes.video_type;
        }

        /*
        visible
         */
        boolean deal_visible = true;
        if(deal_visible){
            TermQueryBuilder baseFilter = QueryBuilders.termQuery("Metadata.Visible", 0);
            NotQueryBuilder notVisibleFilter = QueryBuilders.notQuery(baseFilter);
            filter.add(notVisibleFilter);
        }




        //****************************************
        /*
		* from
		* size
		*/
        Integer from = param.getFrom();
        Integer size = param.getSize();

        /*
        * <sorttype>
         */
        String sorttype = param.getSorttype();
        if(sorttype!=null && !sorttype.isEmpty()){
            sorttype = SearchParameters.SortTypes.getSortType(sorttype);
        }
        else{
            sorttype = SearchParameters.SortTypes.relevancy;
        }
        String sort_field = SearchParameters.SortTypes.sort_fields.get(sorttype);
        SortOrder sort_order = SearchParameters.SortTypes.sort_orders.get(sorttype);
        SortBuilder sort = new FieldSortBuilder(sort_field).order(sort_order).ignoreUnmapped(true);

        /*
		* <explain>
		 */
        Boolean is_explain = EncodeDecode.is_explain(param.getExplain());

        //*****************************************
        /*
        * query + filter
         */
        FilteredQueryBuilder filteredQuery = QueryBuilders.filteredQuery(
                query,
                filter
        );

		/*
		* <dsl>
		 */
        Boolean is_dsl = EncodeDecode.is_dsl(param.getDsl());
        if(is_dsl == true){
            SearchRequestBuilder request = this.elasticsearchDao.genSearchRequset(filteredQuery, from, size ,sort, levels,is_explain);
            return request.toString();
        }

        //****************************************
        SearchResponse response = elasticsearchDao.search(filteredQuery, from, size ,sort, levels,is_explain);
        //return response.toString();
        return JsonFormatter.formatSearchResponse(response);
    }


    /*************************************************************
     *
     * @param args
     */
    public static void main(String[] args){
        ComplexSearchService_V2 service = new ComplexSearchService_V2(
                //new ObjectMapper(),
                new ElasticSearchDao("debug") //, new TitanGraphDao("debug")
                 );
        ComplexParameter params = new ComplexParameter();
        ObjectMapper mapper = new ObjectMapper();
        String json = "{\"fulltext\":\"北京市车流\" , \"dsl\":\"true\"}";
        try {
            params = mapper.readValue(json, ComplexParameter.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
        String result = service.complexSearch(params);
        System.out.println(result);
    }
}
